﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Microsoft.Cci;
using PartialExpressionCompleter.Util;

using Logging;

namespace PartialExpressionCompleter.Query
{
    public class MethodCache
    {
        private static readonly Dictionary<string, MethodCache> methodCaches =
            new Dictionary<string, MethodCache>();

        private readonly ITypeDefinition forType;
        private readonly string typeName;
        private readonly int staticCount;
        private readonly MethodCache[] parents;
        private readonly List<IMethodDefinition> staticMethods;

        private static List<IMethodDefinition> allRefStaticMethods = null;

        private MethodCache(ITypeDefinition forType)
        {
            var start = DateTime.Now;

            this.forType = forType.GetGenericType().ResolvedType;
            this.typeName = this.forType.GetGenericErasedType().ToString();

            var isManagedPointer = forType is IManagedPointerType;
            if (isManagedPointer)
            {
                if(allRefStaticMethods == null)
                {
                    allRefStaticMethods = CCIUtil.GetAllStaticMethods()
                        .Where(m => m.Parameters.Any(p => p.IsByReference || p.IsOut))
                        .ToList();
                }
            }
            var parentTypes = this.forType.ResolvedType
                .GetOnlySuperTypes();
            if(parentTypes.IsEmpty() && forType.IsInterface)
            {
                parentTypes = CCIUtil.GetType("System.Object").Singleton();
            }
            // TODO Complete assignability for primitive types.
            if (typeName.Equals("System.Byte"))
            {
                parentTypes = CCIUtil.GetType("System.Int16").Cons(parentTypes);
                parentTypes = CCIUtil.GetType("System.Int32").Cons(parentTypes);
                parentTypes = CCIUtil.GetType("System.Int64").Cons(parentTypes);

                parentTypes = CCIUtil.GetType("System.UInt16").Cons(parentTypes);
                parentTypes = CCIUtil.GetType("System.UInt32").Cons(parentTypes);
                parentTypes = CCIUtil.GetType("System.UInt64").Cons(parentTypes);
            }
            else if (typeName.Equals("System.Int16"))
            {
                parentTypes = CCIUtil.GetType("System.Int32").Cons(parentTypes);
                parentTypes = CCIUtil.GetType("System.Int64").Cons(parentTypes);

                parentTypes = CCIUtil.GetType("System.UInt32").Cons(parentTypes);
                parentTypes = CCIUtil.GetType("System.UInt64").Cons(parentTypes);
            }
            else if (typeName.Equals("System.Int32"))
            {
                parentTypes = CCIUtil.GetType("System.Int64").Cons(parentTypes);

                parentTypes = CCIUtil.GetType("System.UInt64").Cons(parentTypes);
            }
            else if (typeName.Equals("System.Char"))
            {
                parentTypes = CCIUtil.GetType("System.Int32").Cons(parentTypes);
                parentTypes = CCIUtil.GetType("System.Int64").Cons(parentTypes);

                parentTypes = CCIUtil.GetType("System.UInt32").Cons(parentTypes);
                parentTypes = CCIUtil.GetType("System.UInt64").Cons(parentTypes);
            }
            else if (typeName.Equals("System.Single"))
            {
                parentTypes = CCIUtil.GetType("System.Double").Cons(parentTypes);
            }
            this.parents = parentTypes
                .Select(t => GetMethodCacheForType(t))
                .Distinct()
                .ToArray();
            if (isManagedPointer)
            {
                // TODO Actually prune this or don't bother?
                this.staticMethods = allRefStaticMethods;
            }
            else
            {
                this.staticMethods = CCIUtil.GetAllStaticMethods()
                    .Where(m => !(m.IsConstructor || m.IsStaticConstructor)
                        && m.GetParameterTypes()
                            .Select(t => t.ResolvedType.GetGenericErasedType().ToString())
                            .Contains(typeName))
                    .ToList();
            }
            /*this.instanceMethods = this.forType.Methods
                .Where(m => !(m.IsConstructor || m.IsStaticConstructor)
                    && m.HasThisParameter())
                .ToList();*/
            this.staticCount = staticMethods.Count
                + parents.Select(p => p.staticMethods.Count).Sum();
            /*this.instanceCount = instanceMethods.Count
                + parents.Select(p => p.instanceMethods.Count).Sum();*/

            Logger.PrintLine("Time to generate method cache: "
                + (DateTime.Now - start).TotalMilliseconds
                + " ms for " + this.ToString());

            // XXX DEBUG
            this.EnsureNotInParents();
        }
        public static MethodCache GetMethodCacheForType(ITypeDefinition type)
        {
            MethodCache res;
            type = type.GetGenericErasedType().ResolvedType;
            string typeStr = type.ToString();
            if (!methodCaches.TryGetValue(typeStr, out res))
            {
                res = new MethodCache(type);
                methodCaches[res.typeName] = res;
            }
            return res;
        }

        private void EnsureNotInParents()
        {
            foreach (var p in parents)
            {
                p.EnsureNotInParents(this.typeName);
            }
        }
        private void EnsureNotInParents(string typeName)
        {
            if (this.typeName.Equals(typeName))
            {
                throw new Exception("Loop with type " + typeName);
            }
            foreach (var p in parents)
            {
                p.EnsureNotInParents(typeName);
            }
        }

        public IEnumerable<IMethodDefinition> AllStaticMethods
        {
            get
            {
                return staticMethods.Concat(parents.Select(p => p.staticMethods).Concat());
            }
        }
        public int StaticCount
        {
            get
            {
                return this.staticCount;
            }
        }

        /*
        public IEnumerable<IMethodDefinition> AllInstanceMethods
        {
            get
            {
                return instanceMethods.Concat(parents.Select(p => p.instanceMethods).Concat());
            }
        }
        public int InstanceCount
        {
            get
            {
                return this.instanceCount;
            }
        }*/

        public override string ToString()
        {
            return "MethodCache for " + typeName
                + " with " + staticMethods.Count + " static methods "
                + "(" + staticCount + " total static methods)"
                //+ " and " + instanceMethods.Count + " instance methods "
                //+ "(" + instanceCount + " total instance methods)"
                + " and " + parents.Length + " supertypes";
        }

        public override int GetHashCode()
        {
            return typeName.GetHashCode();
        }
        public override bool Equals(object obj)
        {
            return obj == this;
        }
    }

    public static class MethodCacheUtil
    {
        public static MethodCache GetSmallestMethodCacheForTypes(
            IEnumerable<ITypeDefinition> types)
        {
            return types
                .Select(t => MethodCache.GetMethodCacheForType(t))
                .ArgMin(c => c.StaticCount);
        }
    }
}
